package ru.amse.ilyin.parser;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;

class ConstantPool {
	private static final int BITS_IN_BYTE = 8;
	
	private static final int CONSTANT_CLASS = 7;
	private static final int CONSTANT_FIELDREF = 9;
	private static final int CONSTANT_METHODREF = 10;
	private static final int CONSTANT_INTERFACEMETHODREF = 11;
	private static final int CONSTANT_STRING = 8;
	private static final int CONSTANT_INTEGER = 3;
	private static final int CONSTANT_FLOAT = 4;
	private static final int CONSTANT_LONG = 5;
	private static final int CONSTANT_DOUBLE = 6;
	private static final int CONSTANT_NAMEANDTYPE = 12;
	private static final int CONSTANT_UTF8 = 1;
	
	private final ArrayList<Byte> bytes = new ArrayList<Byte>();

	private int[] positions;
	
	/**
	 * Parses constant pool from the specified input stream.
	 * 
	 * @param input input stream to parse from
	 * @throws IOException if an i/o error occurs
	 */
	public void parse(DataInputStream input) throws IOException {
		final int constantPoolCount = input.readUnsignedShort();
		positions = new int[constantPoolCount];
		
		// the constant_pool table is indexed from 1 to constant_pool_count-1
		for (int i = 1; i < constantPoolCount; i++) {
			positions[i] = bytes.size();
			if (parseConstantPoolInfo(input)) {
				// 8-byte constants take up two entries
				i++;
			}
		}
	}
	
	
	private boolean parseConstantPoolInfo(DataInput input) throws IOException {
		final int tag = input.readUnsignedByte();
		bytes.add((byte) tag);
		
		switch(tag) {
		case CONSTANT_CLASS:
			readBytesToPool(input, 2);
			break;
		case CONSTANT_FIELDREF:
		case CONSTANT_METHODREF:
		case CONSTANT_INTERFACEMETHODREF:
			readBytesToPool(input, 4);
			break;
		case CONSTANT_STRING:
			readBytesToPool(input, 2);
			break;
		case CONSTANT_INTEGER:
		case CONSTANT_FLOAT:
			readBytesToPool(input, 4);
			break;
		case CONSTANT_LONG:
		case CONSTANT_DOUBLE:
			readBytesToPool(input, 8);
			return true;
		case CONSTANT_NAMEANDTYPE:
			readBytesToPool(input, 4);
			break;
		case CONSTANT_UTF8:
			final byte byte1 = (byte) input.readUnsignedByte();
			final byte byte2 = (byte) input.readUnsignedByte();
			bytes.add(byte1);
			bytes.add(byte2);
			final int length = getUnsignedShort(byte1, byte2);
			readBytesToPool(input, length);
			break;
		default:
			//TODO replace with AnalyzerException
			throw new RuntimeException("unknown tag: " + tag);
		}
		
		return false;
	}
	
	/**
	 * Returns a class name which is stored in the constant pool
	 * at the specified index.
	 * 
	 * @param poolIndex index in the constant pool table
	 * @return class name from the pool at the specified index
	 */
	public String getClassName(int poolIndex) {
		final int byteIndex = positions[poolIndex];
		
		assert (bytes.get(byteIndex) == CONSTANT_CLASS) : "wrong tag";
		
		return getString(positions[getUnsignedShort(
				bytes.get(byteIndex + 1),
				bytes.get(byteIndex + 2)
		)]);
	}
	
	private String getString(int byteIndex) {
		assert (bytes.get(byteIndex) == CONSTANT_UTF8) : "wrong tag";
		
		final int length = getUnsignedShort(
				bytes.get(byteIndex + 1),
				bytes.get(byteIndex + 2)
		);
		
		final StringBuffer buffer = new StringBuffer();
		
		for (int i = byteIndex + 3; i < byteIndex + 3 + length; i++) {
			final char x = byteToChar(bytes.get(i));
			if ((x & 0x80) == 0) {
				buffer.append(x);
				continue;
			}
			
			final char y = byteToChar(bytes.get(i));
			i++;
			if ((x & 0x20) == 0) {
				buffer.append(((x & 0x1f) << 6) + (y & 0x3f));
				continue;
			}
			
			final char z = byteToChar(bytes.get(i));
			i++;
			buffer.append(((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f));
		}
		
		return buffer.toString();
	}
	
	private void readBytesToPool(DataInput input, int bytesNumber) 
			throws IOException {
		for (int i = 0; i < bytesNumber; i++) {
			bytes.add(input.readByte());
		}
	}
	
	private int getUnsignedShort(byte byte1, byte byte2) {
		return (getUnsignedByte(byte1) << BITS_IN_BYTE) +
			   getUnsignedByte(byte2);
	}
	
	private int getUnsignedByte(byte signedByte) {
		return (signedByte < 0) 
				? ((-signedByte)  | 0x80) 
				: signedByte;
	}

	private char byteToChar(byte b) {
		return (char) (b & 0xFF);
	}
}
